package org.desktop.intbox.business.files.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import org.desktop.intbox.business.files.IFileTreeNode;
import org.desktop.intbox.business.files.ITreeNodeVisitor;

/**
 * 
 * @author nikolay.grozev
 * 
 */
public class FileTreeNode implements IFileTreeNode {

	private FileTree tree;
	private FileTreeNode parent;
	private int depth;
	private final Set<FileTreeNode> children = new LinkedHashSet<>();
	private final Set<FileTreeNode> unmodifiableChildren = Collections.unmodifiableSet(children);

	private Set<FileTreeNode> displayChildren = null;

	private String name;
	private String categoryName;
	private long size;

	public FileTreeNode(final String name) {
		this(name, 0L);
	}

	public FileTreeNode(final String name, final long size) {
		this(name, size, null);
	}

	public FileTreeNode(final String name, final FileTree tree) {
		this(name, 0L, tree);
	}

	public FileTreeNode(final String name, final long size, final FileTree tree) {
		setName(name);
		this.size = size;
		this.tree = tree;
		parent = null;
		depth = 1;
	}

	@Override
	public FileTree getTree() {
		return tree;
	}

	public void setTree(final FileTree tree) {
		if (this.tree == null || !this.tree.equals(tree)) {
			this.tree = tree;
			for (FileTreeNode child : getChildren()) {
				child.setTree(tree);
			}
		}
	}

	@Override
	public FileTreeNode getParent() {
		return parent;
	}

	@Override
	public Collection<? extends FileTreeNode> getChildren() {
		return unmodifiableChildren;
	}

	@Override
	public Collection<? extends FileTreeNode> getDisplayChildren() {
		return displayChildren == null ? getChildren() : Collections.unmodifiableSet(displayChildren);
	}

	public void setDisplayChildren(final Set<FileTreeNode> displayChildren) {
		this.displayChildren = displayChildren;
	}

	@Override
	public String getName() {
		return name;
	}

	private void setName(final String name) {
		this.name = name;
		if (getChildren().isEmpty()) {
			int indx = name.lastIndexOf('.');
			this.categoryName = indx > 1 ? this.name.substring(indx + 1).toLowerCase().intern() : "";
		}
	}

	@Override
	public String getCategoryName() {
		return getChildren().isEmpty() ? categoryName : "";
	}

	public void setCategoryName(final String categoryName) {
		this.categoryName = categoryName;
	}

	@Override
	public Long getSize() {
		return size;
	}

	@Override
	public int getDepth() {
		return depth;
	}

	@Override
	public void visitTreeStructure(final ITreeNodeVisitor visitor) {
		visit(visitor, new INodesVisitSelector() {
			@Override
			public Collection<? extends FileTreeNode> getNodesToVisit(final FileTreeNode node) {
				return node.getChildren();
			}
		});
	}

	@Override
	public void visitDisplayedNodes(final ITreeNodeVisitor visitor) {
		visit(visitor, new INodesVisitSelector() {
			@Override
			public Collection<? extends FileTreeNode> getNodesToVisit(final FileTreeNode node) {
				return node.getDisplayChildren();
			}
		});
	}

	private void visit(final ITreeNodeVisitor visitor, final INodesVisitSelector selector) {
		if (selector.getNodesToVisit(this).size() > 0) {
			visitor.preVisitBranchNode(this);
		}
		visitor.visit(this);
		for (FileTreeNode child : selector.getNodesToVisit(this)) {
			child.visit(visitor, selector);
		}

		if (selector.getNodesToVisit(this).size() > 0) {
			visitor.postVisitBranchNode(this);
		}
	}

	public FileTreeNode addChild(final String name) {
		FileTreeNode node = new FileTreeNode(name);
		node.attachToParent(this);
		return node;
	}

	public FileTreeNode addChild(final String name, long size) {
		FileTreeNode node = new FileTreeNode(name, size, tree);
		node.attachToParent(this);
		return node;
	}

	protected synchronized void attachToParent(final FileTreeNode parent) {
		detachFromParent();
		this.parent = parent;
		this.tree = parent.getTree();

		int maxChildDepth = parent.getMaxChildDepth();
		updateParentDepthWith(maxChildDepth >= getDepth() ? 0 : getDepth() - maxChildDepth);

		updateParentSizeWith(this.size);
		parent.children.add(this);
	}

	protected synchronized void detachFromParent() {
		if (parent != null) {
			parent.children.remove(this);
			updateParentSizeWith(-this.size);

			int maxChildDepth = parent.getMaxChildDepth();
			updateParentDepthWith(maxChildDepth >= getDepth() ? 0 : maxChildDepth - getDepth());
		}
		parent = null;
	}

	private void updateParentSizeWith(final long diff) {
		if (parent != null && diff != 0) {
			parent.size += diff;
			parent.updateParentSizeWith(diff);
		}
	}

	private void updateParentDepthWith(final int diff) {
		if (parent != null && diff != 0) {
			parent.depth += diff;
			parent.updateParentDepthWith(diff);
		}
	}

	private int getMaxChildDepth() {
		int maxDepth = 0;
		for (FileTreeNode child : children) {
			if (child.getDepth() > maxDepth) {
				maxDepth = child.getDepth();
			}
		}
		return maxDepth;
	}

	private static interface INodesVisitSelector {
		Collection<? extends FileTreeNode> getNodesToVisit(final FileTreeNode node);
	}
}
